用 Three.js 写了一个涵盖了大部分基础3D功能的综合场景

您所在的位置:网站首页 3d模型 格式 用 Three.js 写了一个涵盖了大部分基础3D功能的综合场景

用 Three.js 写了一个涵盖了大部分基础3D功能的综合场景

#用 Three.js 写了一个涵盖了大部分基础3D功能的综合场景| 来源: 网络整理| 查看: 265

前言

收藏时顺便可以点个赞哈哈哈😂

22年1月23日更新:由于知识产权问题,源码在个人GitHub永久下架,代码片段在文章还是有很多的。

作者在实习期间,实现了一个 Web3D 风机综合场景Demo(实习答辩所用,当时未上线)。主要是对 图扑的风机场景 的改进。

可能是因为只是图扑官网的demo,所以可能上线较为仓促,所以图扑的风机场景存在以下问题

首次加载速度慢(主要和3D模型相关) 渲染低效(主要和3D模型相关) 不可场景化(不可改变天气,动画等) 交互性差(基本没有交互功能)

所以我后面对图扑的 demo 进行了如下改进

增加场景化 —— 根据风速变化改变叶片转速、根据天气预报来模拟风场天气 提高渲染效率 —— 使用glTF格式模型 增强交互性 —— 增加控件选项,选择权交给用户 演示视频 —— 风机3D场景演示视频 __ 哔哩哔哩

Web3D的基础知识

3d的基本知识主要是基于计算机图形学的

包括场景Scene、摄像机Camera、渲染器Renderer、几何体Geometry、光照模型Light、纹理贴图Texture、材质Material等

具体的可以参考 郭隆邦大佬的博客,确实是有比较多的内容,这里就不再展开了

3D模型的选择

3D 模型格式的选择是最简单的一步,但是可能也是对渲染效率最关键的一步。

图扑的 3D格式 选用的是 OBJ+MTL, 这也是他加载速度慢和渲染负载高的重要因素之一,所以我首先从3D模型格式入手,进行第一步改进。

常用Web3D模型的区别

在选择 3D格式 之前,我们需要了解有哪些主流的 3D格式,各自又有什么特点

glTF

glTF 是一个非常全面的格式,几乎支持所有的常见功能,支持颜色、动画、CSG、细节网格工作、纹理、相机、光线、相对定位等重要功能。

glTF 是一个中转格式,不需要像其他格式一样创建 importer、converter,直接可以被相关application使用。

STL

STL仅支持几何图形的简单表达功能,其他类似于颜色、材质、光照、纹理等性质一概没有,所以几乎是一个不可用的 3D 格式

OBJ OBJ 支持某些功能,如简要表达,CSG,颜色和材料。但是没有动画、纹理、相机等重要的3D模型性质。 比如对于OBJ 格式的模型来说,如果需要显示想要的纹理,则要借助额外的mtl文件来显示纹理。

FBX

更准确的说,FBX模型是一种通用模型格式,而不是像 OBJ 一样的3D模型文件格式,支持所有主要的三维数据元素以及二维、音频和视频媒体元素,还包含动画、材质特性、贴图、骨骼动画、灯光、摄像机等信息。

FBX格式主要支持多边形(Polygons)游戏模型、曲线(Curves)、表面(Surfaces)、点组材质(Point Group Materials)。但是没有提供详细的网格(Detailed Mesh),其他和gltf一样完善,较为适用于游戏。

图表总结

image.png

为什么我使用glTF格式

glTF 的底层工作原理还是比较多了,我只是在这里解释一下它作为 Web3D 格式的一些优点,如果需要详细学习 glTF 的同学可以点击 glTF-Tutorials 和 如何在页面极速渲染3D模型

免除额外的模型数据转换,提高解析速度 glTF的目标是作为一个中转格式,而不是另一个新的3D数据格式 使用JSON来描述场景结构,可以方便地被应用程序分析处理。但是3D对象的几何数据和纹理数据通常不被包含在JSON文件中,它们被存储在外部的文件或二进制文件中,JSON文件中只包含了到这些外部文件的链接。 3D数据以一种可以被大多数图形API直接使用的方式(以二进制的形式)进行存储,不需要应用程序进行解码或预处理操作。

image.png

其他格式,比如OBJ在运行相关application使用前,为了读取不同的输入文件格式,所以必须解析场景结构,并且必须将 数值型几何数据转换为图形 API 所需的格式。 所以每个运行时应用程序都必须为其支持的所有文件格式创建很多importer、converter,既占内存,也降低了渲染效率。

image.png

改变动画较为方便 图扑的风机场景demo是基于OBJ格式的动画,但是由于 OBJ 没有自带动画,所以在修改动画时会比较麻烦 glTF修改动画非常方便,关于动画的详细内容我会放在下文继续讲。

改变glTF 3D模型动画

图扑的风机叶片转速是固定的,我希望其能场景化,根据后端传来的风速数据进行相应的改变,也就是说风速越快,叶片转的越快,这就涉及到了 改变模型的动画

风速.gif

搞清glTF如何形成动画

glTF格式的3D模型是可以自带动画的(这一点和OBJ区别很大,OBJ是不可以自带动画的)

glTF格式动画的底层存放以及动画插值等原理请看 glTF格式详解(示例:一个简单的动画) 和 glTF格式详解(动画)

如果不想深究的同学可以跳过以上两个链接,本篇文章只讲在Three.js的loader载入glTF格式的3D模型之后,我们如何改变动画,专注于实现而不展开原理。

改变模型的动画 首先通过 GLTFLoader 载入带有动画的模型 import turbine from '../../assets/3d-gltf-model/turbine.glb'; loadTurbine = () => { const loader = new GLTFLoader() if (this.scene.getObjectByName('turbine')) { let removeTurbine = this.scene.getObjectByName('turbine') this.scene.remove(removeTurbine) } loader.load(turbine, object => { this.matrixTurbine = object; let mesh = object.scene; mesh.position.set(0, -2, 0); this.scene.add(mesh); }) } loader 载入后的 matrixTurbine对象属性

github 的示例中的glTF模型是自带动画的,所以在load模型之后形成一个对象之后,动画相关的数据就存放在 matrixTurbine对象的属性中

matrixTurbine对象 的 animations属性是一个数组,里面存了AnimationsClip,是一组可重复使用的关键帧轨迹,代表了gltf自带的动画。

AnimationsClip 中的 duration 是这组动画持续的时间,tracks数组 存放了该动画时间内的动画帧键值对,是一组关键帧轨道,也就是每一个时间单位所对应的部件位置(因为该 glTF模型 有透明和不透明两种展示模式,所以图中的 tracks数组 有两个 QuaternionKeyframeTrack对象)。不断重复这组关键帧轨迹,就可以得到持续不断的动画

QuaternionKeyframeTrack 对象的 times属性 和 values属性

QuaternionKeyframeTrack 对象的 times属性 是一个关键帧的时间数组;QuaternionKeyframeTrack 对象的 values属性 是一个与时间数组中的时间点对应的值数组。

怎么理解呢?

我们先打印这两个属性看一看是什么。我们可以看到,times的长度是values的四分之一

console.log(this.matrixTurbine.animations[0].tracks[1].times) console.log(this.matrixTurbine.animations[0].tracks[1].values) // Float32Array(145) // Float32Array(580)

我们可以想象 values属性 就是一组3D对象需要做的动作,times是这些动作所完成的时刻,比如在上文中,每一个times数组的每一个item就对应了values中的四个item,通过glTF内部的动画插值算法来形成一个完整连续的动画。 具体原理可能又得开一篇文章讲,在此先挖个坑,暂且形象地理解一下。

image.png

通过改变 times属性数组 来改变动画的快慢

之前我们提到了animations的duration属性,其就是一轮动画持续的时间,times数组实际上是对该时间的均匀分割。比如duration = 6,此时 Float32Array(145) 就是为 [0, 0.0416666679084301, 0.0833333358168602, 0.125 ...... 5.958333492279053, 6]。

如果我要改变动画的快慢,只需要根据后端传来的 风速windSpeed 来成比例的修改 times数组 和 duration

// 改变风机转速的部分 this.matrixTurbine.animations[0].tracks[1].times = this.changeArr(object.animations[0].tracks[1].times)// 控制透明的风机动画速度 this.matrixTurbine.animations[0].tracks[0].times = this.changeArr(object.animations[0].tracks[0].times) // 控制有材质的风机动画速度 this.matrixTurbine.animations[0].duration = 6 / (windSpeed / 5) function changeArr(arr){ return arr.map((a) => a / (windSpeed / 5)) } 给无动画的gltf模型增加动画

这个例子主要讲的是如何改变有动画的glTF模型,如果遇上没有自带动画的3D模型,可以尝试一下操作:

不是直接使用构造函数实例化 AnimationClip,您可以使用其静态方法之一来创建 AnimationClips:从 JSON (解析)、从变形目标序列 (CreateFromMorphTargetSequence、CreateClipsFromMorphTargetSequences) 或从动画层次结构 (parseAnimation) - 如果您的模型尚未在其几何体的动画数组中保存 AnimationClips。

雷暴天气的模拟

还有一个需求就是希望可以根据后端的天气数据接口渲染天气,我就以雷暴天气作为例子来展开关于纹理、材质和几何体相关的知识。

雷暴天气演示

我们可以看到,不定时会有闪电的出现,而且乌云密布且呈现流动的状态

weather.gif

乌云密布的实现

该实现主要和 几何体、纹理、材质相关

基于3D空间坐标,在风机上方新建数个位置随机的平面几何体,同时设置一个非物理的只计算漫反射关注光照朗伯材质

我们先要知道一个基础的3D知识:一个网格(Mesh)由几何体和材质组成,整个3D场景的Object可以近似看作数个Mesh的组合。 一片乌云即可作为一个Mesh,由几何体和材质组成。代码中的 cloud 就是一个由几何体和材质组成的一个Mesh,表示基于以三角形为 polygon mesh(多边形网格)的物体的类。

很明显,该Mesh的几何性质可以近似为一个平面,数个 平面几何体PlaneBufferGeometry 被随机平铺开来就形成了上空中的云层,作为 “乌云” 的几何载体。

由于在闪电照亮乌云时,乌云本身并不会出现近似于镜面反射的效果,更接近于漫反射的效果,所以我们可以使用 Three.js 的 MeshLambertMaterial 材质构造函数来创建暗淡的并不光亮的表面,主要反映的是光线的漫反射。

// cloudGeo既是上面提到的几何体,cloudMaterial是包含了纹理(texture)的材质,纹理的性质我们在下文会讲到 const cloudGeo = new THREE.PlaneBufferGeometry(400, 400); const cloudMaterial = new THREE.MeshLambertMaterial({ map: texture, transparent: true }); Mesh中的材质性质如果需要起视觉作用,需要和纹理进行结合,也就是常说的“贴图”

通过 let cloud = new THREE.Mesh(cloudGeo, cloudMaterial) 这行代码,框架内置的投影方程和映射函数将 Mesh 映射到纹理空间,然后纹理空间检索设置的smoke.png中的颜色值,然后值变换函数对检索见过进行值变换用于改变几何体表面属性,换句话说就是把smoke.png“贴”到了所创建的包含了几何体和材质的Mesh上。

设置多个Mesh的位置

本代码中设置了5个 Mesh,随机地放在了3D风机的上方的空间上,看起来就像“罩住”了风机,读者在自己实现的时候可以借此熟悉Three.js的三维坐标系。

for (let p = 0; p < 5; p++){ let cloud = new THREE.Mesh(cloudGeo, cloudMaterial); cloud.position.set( Math.random() * 10 + 90, Math.random() * 20 + 15, -Math.random() * 50 - 80 ); } 乌云流动的实现

rotation:表示物体绕x,y,z轴旋转的弧度。在动画函数中,将所有乌云Mesh绕z轴进行旋转,然后就是使用requestAnimationFrame进行动画的一个套路了,在此不再赘述

function animate() { cloudParticles.forEach(p => { // cloudParticles 是包含 数个乌云Mesh 的数组 p.rotation.z -= 0.002; }); } requestAnimationFrame(animate); 雷暴闪电的实现

闪电的实现和光照模型有关,Three.js中的光种类主要包含环境光AmbientLight、 平行光DirectionalLight 和 点光源PointLight。

使用的光照类型

点光源的光线是发散的,无法直接定义它的光线方向,这与闪电的发光特性类似,所以我们使用点光源来模拟闪电。

在每一帧动画渲染时,可能都会在乌云Mesh的上方的随机位置产生一个点光源,光源照到乌云和风机模型,光线会和两者的几何纹理以及顶点法向量进行计算,从而改变大量顶点的颜色,重新进入渲染管线,从而模拟出闪电的效果

wholeFlashGroup.name = 'flash' const flash = new THREE.PointLight(0xe0ffff, 10000, 0, 2); flash.position.set(100, 100, -110); wholeFlashGroup.add(flash); function animate() { if (Math.random() > 0.90 || flash.power > 220) { if (flash.power < 100) flash.position.set( Math.random() * 30 + 80, Math.random() * 20 + 10, Math.random() * 3 - 100 ); flash.power = 50 + Math.random() * 500; } requestAnimationFrame(animate); } 总结:关于雷暴天气部分的主要代码 // 闪电乌云特效 loadFlash = () => { const wholeFlashGroup = new THREE.Group(); wholeFlashGroup.name = 'flash' const ambient = new THREE.AmbientLight(0x555555); wholeFlashGroup.add(ambient); const directionalLight = new THREE.DirectionalLight(0xffeedd); directionalLight.position.set(0, 0, 1); wholeFlashGroup.add(directionalLight); const flash = new THREE.PointLight(0xe0ffff, 10000, 0, 2); flash.position.set(100, 100, -110); wholeFlashGroup.add(flash); this.myRef.current.appendChild(renderer.domElement); let loader = new THREE.TextureLoader(); loader.load(smoke, (texture) => { const cloudGeo = new THREE.PlaneBufferGeometry(400, 400); const cloudMaterial = new THREE.MeshLambertMaterial({//一种用于无光泽表面的材料,没有镜面高光。该材料使用基于非物理的朗伯模型来计算反射率 map: texture, transparent: true }); for (let p = 0; p < 5; p++) { let cloud = new THREE.Mesh(cloudGeo, cloudMaterial); cloud.position.set( Math.random() * 10 + 90, Math.random() * 20 + 15, -Math.random() * 50 - 80 ); cloud.rotation.x = 1.16; cloud.rotation.y = -0.12; cloud.rotation.z = Math.random() * 360; cloud.material.opacity = 0.4; cloudParticles.push(cloud); wholeFlashGroup.add(cloud); } this.scene.add(wholeFlashGroup) animate(); }); const animate = () => { cloudParticles.forEach(p => { // cloudParticles 是包含 数个乌云Mesh 的数组 p.rotation.z -= 0.002; }); if (Math.random() > 0.90 || flash.power > 220) { if (flash.power < 100) flash.position.set( Math.random() * 30 + 80, Math.random() * 20 + 10, Math.random() * 3 - 100 ); flash.power = 50 + Math.random() * 500; } flashAnimatation = requestAnimationFrame(animate); } }

glTF 3D模型的交互 交互的第一步:鼠标点击获得3D对象 将鼠标的屏幕坐标转换为标准设备坐标

获得3D对象进行交互并不像在2D平面那么简单,核心矛盾在于模型对象在内存中是以3维的形式存在的,但是我们鼠标点击的坐标都是屏幕中基于XY轴的2维坐标

所以首先就要先将鼠标点击的2维屏幕坐标位置变成一个 Three.js 的标准化设备坐标(下文中“注”的反过程),Three.js 通过标准化设备坐标就可以创建拾取射线来获得3维空间中的对象。

注:OpenGL希望在所有顶点着色器运行后,所有我们可见的顶点都变为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个方向上的坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素。

// 通过鼠标事件的返回的事件对象event的坐标属性clientX、clientY获得鼠标点击位置相对浏览器窗口的坐标 // 然后转化为标准设备坐标 this.mouse.x = (event.clientX / w) * 2 - 1; this.mouse.y = -(event.clientY / h) * 2 + 1; 创建拾取射线获取相交的对象

拾取射线 就像一个穿行在3维空间的一个烤串签子,把所有与它相交的3D对象“串起来”形成一个数组,数组的第一个item一般就是我们点击得到的对象。

通过raycaster.setFromCamera(mouse, camera),即标准化设备坐标的鼠标坐标和camera创建拾取射线

通过raycaster.intersectObject(equipment, true)计算与拾取射线相交的对象,返回射线旋转的所有对象,该方法的参数是一个Object3D对象构成的数组,表示射线对象的选择范围,凡是选中的都会以数组的形式返回,如果两个mesh屏幕坐标位置是重合的,那么都会被选中

总结:鼠标点击获得3D模型对象的代码 function onPointerClick (event){ const [w, h] = [window.innerWidth, window.innerHeight]; const {mouse, equipment, raycaster} = this; this.mouse.x = (event.clientX / w) * 2 - 1; this.mouse.y = -(event.clientY / h) * 2 + 1; raycaster.setFromCamera(mouse, this.camera); const intersects = raycaster.intersectObject(equipment, true); if (intersects.length


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3